export type Effect<N extends string = string, A extends any[] = any[], in O = never> =
  { nm: N, arg: A, __out?: [(_: O) => void, never] }

type OutOf<E extends Effect> =
  E['__out'] extends [(_: infer O) => void, never] | undefined ? O : never

type ExtEff<E extends Effect, K extends string> = Extract<E, Effect<K>>

type ExcEff<E extends Effect, K extends string> = Exclude<E, Effect<K>>

export type EffDiff<E extends Effect, E2 extends Effect> = ExcEff<E, E2['nm']>

export type Stream<E extends Effect, R> = Generator<E, R, unknown>

type Continuation<E extends Effect, O, R> =
  ((result: O, tco?: false) => Stream<E, R>) & ((result: O, tco: true) => typeof TCO)

type Handler<E extends Effect, E0 extends Effect, E2 extends Effect, R> =
  (cont: Continuation<E0, OutOf<E>, R>, ...arg: E['arg']) => Stream<E0 | E2, R | typeof TCO>

export type Handlers<E extends Effect, E0 extends Effect, E2 extends Effect, R> = {
  [K in E['nm']]: Handler<ExtEff<E, K>, E0, E2, R>
}

export type Return<E2 extends Effect, R1, R2> = (result: R1) => Stream<E2, R2>

export type GEHandler<E extends Effect, E2 extends Effect, R> = <E0 extends Effect>(
  cont: Continuation<E0, OutOf<E>, R>, ...arg: E['arg']
) => Stream<E0 | E2, R | typeof TCO>

export type GEHandlers<E extends Effect, E2 extends Effect, R> = {
  [K in E['nm']]: GEHandler<ExtEff<E, K>, E2, R>
}

export type GAHandler<E extends Effect, E2 extends Effect> = <E0 extends Effect, R>(
  cont: Continuation<E0, OutOf<E>, R>, ...arg: E['arg']
) => Stream<E0 | E2, R | typeof TCO>

export type GAHandlers<E extends Effect, E2 extends Effect> = {
  [K in E['nm']]: GAHandler<ExtEff<E, K>, E2>
} & { return?: never }

export type CheckStream<E1 extends Effect, E extends Effect, R> =
  ExtEff<E1, E['nm']> extends E ? Stream<E1, R> : never

export type CheckStream0<E extends Effect, E0 extends Effect, R> =
  ExtEff<E0, E['nm']> extends never ? Stream<E | E0, R> : never

const TCO = Symbol('TCO')

export const eff = <N extends string>(nm: N extends 'return' ? never : N) =>
  function*<A extends any[], O>(...arg: A): Stream<Effect<N, A, O>, O> {
    return (yield { nm, arg }) as O
  }

export const run = <R>(stream: Stream<never, R>): R => {
  const r = stream.next()
  // if (!r.done) throw new Error(`Uncaught Effect ${String(r.value.eff)}`)
  return r.value
}

function* handle_(
  handler: Record<string, Handler<any, any, any, any>> & { return?: Return<any, any, any> },
  stream: Stream<any, any>,
  first: any,
): Stream<any, any> {
  let effect = stream.next(first)

  while (!effect.done) {
    const { nm, arg } = effect.value

    if (!Object.hasOwn(handler, nm)) {
      effect = stream.next(yield effect.value)
      continue
    }

    let tcoResult: any, called = false

    const cont = ((result, tco) => {
      if (called) throw new Error('Continuation has been already called')
      called = true

      tcoResult = result
      if (!tco) return handle_(handler, stream, result)
      return TCO
    }) as Continuation<any, any, any>

    let result = yield* handler[nm]!(cont, ...arg)
    if (result !== TCO) return result
    if (!called) throw 'WTF'
    effect = stream.next(tcoResult)
  }

  const result = effect.value
  if (handler.return) return yield* handler.return(result)
  return result
}

export function handle<E extends E1, E1 extends Effect, E2 extends Effect, R>(
  stream: CheckStream<E1, E, R>,
  handlers: Handlers<E, EffDiff<E1, E>, E2, R> & { return?: never }
): Stream<E2 | EffDiff<E1, E>, R>

export function handle<E extends Effect, E1 extends Effect, E2 extends Effect, R1, R2>(
  stream: CheckStream<E1, E, R1>,
  handlers: Handlers<E, EffDiff<E1, E>, E2, R2> & { return: Return<E2, R1, R2> }
): Stream<E2 | EffDiff<E1, E>, R2>

export function handle(stream: Stream<any, any>, handlers: Record<string, any>) {
  return handle_(handlers, stream, undefined)
}

export function handle1<E extends Effect, E2 extends Effect, N extends string, R>(
  stream: Stream<E, R>, name: N, handler: Handler<ExtEff<E, N>, ExcEff<E, N>, E2, R>
): Stream<E2 | ExcEff<E, N>, R> {
  return handle_({ [name]: handler }, stream, undefined)
}

export function mkHandler<E extends Effect, E2 extends Effect>(
  handlers: GAHandlers<E, E2>
): <E1 extends Effect, R>(stream: CheckStream<E1, E, R>) => Stream<EffDiff<E1, E> | E2, R>

export function mkHandler<E extends Effect, E2 extends Effect, R>(
  handlers: GEHandlers<E, E2, R> & { return?: never }
): <E1 extends Effect>(stream: CheckStream<E1, E, R>) => Stream<EffDiff<E1, E> | E2, R>

export function mkHandler<E extends Effect, E2 extends Effect, R1, R2>(
  handlers: GEHandlers<E, E2, R2> & { return: Return<E2, R1, R2> }
): <E1 extends Effect>(stream: CheckStream<E1, E, R1>) => Stream<EffDiff<E1, E> | E2, R2>

export function mkHandler(handlers: Record<string, any>) {
  return (stream: Stream<any, any>) => handle_(handlers, stream, undefined)
}

export function handleWith<E extends Effect, E2 extends Effect, R1, R2>(
  stream: Stream<E, R1>,
  handler: (stream: Stream<E, R1>) => Stream<E2, R2>
) {
  return handler(stream)
}
